import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Polygon;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.Random;
import java.util.TreeMap;

import java.awt.Frame;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Line2D;
import java.awt.image.BufferedImage;
import java.awt.image.MemoryImageSource;
import java.awt.image.RescaleOp;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.media.opengl.GL2;
import javax.media.opengl.GL2ES2;
import javax.media.opengl.GL3;
import javax.media.opengl.GL4;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLProfile;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.GLEventListener;

import com.jogamp.opengl.util.texture.Texture;
import com.jogamp.opengl.util.texture.awt.AWTTextureIO;

public class Road {
	
	public GLCanvas parent = null;
	public boolean initObjects = true; // set to false after first initialization of road
	public Random randomGenerator = new Random();
	public TreeMap<Integer, ArrayList<Surface>> textureIndexToSurfaces = null;
	public TreeMap<Integer, ArrayList<Wall>> textureIndexToInnerWalls = null;
	public TreeMap<Integer, ArrayList<Wall>> textureIndexToOuterWalls = null;
	public ArrayList<Texture> surfaceTextures = null;
	public ArrayList<Texture> innerWallTextures = null;
	public ArrayList<Texture> outerWallTextures = null;
	public Texture groundTexture = null;
	public TreeMap<Integer, Float> deltaZToBrightness = null;
	public boolean constantBrightness = false;

	CubicSpline distToTheta;
	CubicSpline distToPhi;
	CubicSpline zToLeftX;
	CubicSpline zToRightX;
	CubicSpline zToY;
	CubicSpline zToTheta; // Angle in XZ plane
	CubicSpline zToPhi; // Angle in YZ plane

	float minRoadY = 0.0f;
	float brightnessMantissa = 1.0f / 3.0f;
	float brightnessScalar = 4.0f;
	float markerElevation = 0.0f;
	float markerHeight = 4.0f;
	float markerWidth = 1.0f;
	float markerDepth = 4.0f;
	float roadWidth = 40.0f;
	float pillarHeight = 20.0f;
	float pillarWidth = 2.0f;
	final int maxZ = 15000;
	final int maxViewableZ = 1000;
	final float zStep = 10.0f;
	
	public class Wall implements Drawable {

		private float x0;
		private float y0;
		private float z0;
		private float x1;
		private float y1;
		private float z1;
		private float height;
		private float depth;
		private float textureIndex;

		Wall(float x0, float y0, float z0, float x1, float y1, float z1, float height, float depth, int textureIndex) {
			this.x0 = x0;
			this.y0 = y0;
			this.z0 = z0;
			this.x1 = x1;
			this.y1 = y1;
			this.z1 = z1;
			this.height = height;
			this.depth = depth;
			this.textureIndex = textureIndex;
		}

		public void display(GL2 gl, float z) {
			float scale = 1.0f;
			float z0b = getBrightness((int) Math.abs(z0 - z));
			float z1b = getBrightness((int) Math.abs(z1 - z));
			if(z0b < 1.0f / 255.0f) return;
			gl.glBegin(GL2.GL_POLYGON);
			gl.glColor3f(z0b, z0b, z0b);
			gl.glTexCoord2d(0, 0);
			gl.glVertex3f(x0, y0, z0);
			gl.glColor3f(z1b, z1b, z1b);
			gl.glTexCoord2d(0, 1);
			gl.glVertex3f(x1, y1, z1);
			gl.glTexCoord2d(1, 1);
			gl.glVertex3f(x1 + depth, y1 + height, z1);
			gl.glColor3f(z0b, z0b, z0b);
			gl.glTexCoord2d(1, 0);
			gl.glVertex3f(x0 + depth, y0 + height, z0);
			gl.glEnd();
		}
		
		public boolean pointOver(float x, float z) {
			return false;
		}
		
		public boolean lineIntersects(Point3D currentPosition, Point3D nextPosition) {
			Line2D.Double vehicleVector = new Line2D.Double(currentPosition.x, currentPosition.z, nextPosition.x, nextPosition.z);
			Line2D.Double wallVector = new Line2D.Double(x0, z0, x1, z1);
			return vehicleVector.intersectsLine(wallVector);
		}
		
		public Line2D getLineSegment() {
			return new Line2D.Double(x0, z0, x1, z1);
		}
		
		public float getY() {
			return (y0 + y1) / 2.0f;
		}

	}

	public class Surface implements Drawable {

		Polygon p;
		private float scalePoly = 1000.0f;
		private Wall w0;
		private Wall w1;
		private int textureIndex;

		Surface(Wall w0, Wall w1, int textureIndex) {
			this.w0 = w0;
			this.w1 = w1;
			this.textureIndex = textureIndex;
			p = new Polygon();
			p.addPoint(Math.round(w0.x0 * scalePoly), Math.round(w0.z0 * scalePoly));
			p.addPoint(Math.round(w0.x1 * scalePoly), Math.round(w0.z1 * scalePoly));
			p.addPoint(Math.round(w1.x1 * scalePoly), Math.round(w1.z1 * scalePoly));
			p.addPoint(Math.round(w1.x0 * scalePoly), Math.round(w1.z0 * scalePoly));
		}

		public void display(GL2 gl, float z) {
			float scale = 1.0f;
			float w0z0 = getBrightness((int) Math.abs(w0.z0 - z));
			float w1z0 = getBrightness((int) Math.abs(w1.z0 - z));
			float w0z1 = getBrightness((int) Math.abs(w0.z1 - z));
			float w1z1 = getBrightness((int) Math.abs(w1.z1 - z));
			if(w0z0 < 1.0 / 255.0) return;
			gl.glBegin(GL2.GL_POLYGON);
			gl.glColor3f(w0z0, w0z0, w0z0);
			gl.glTexCoord2d(0, 0);
			gl.glVertex3f(w0.x0, w0.y0, w0.z0);
			gl.glColor3f(w0z1, w0z1, w0z1);
			gl.glTexCoord2d(0, 1);
			gl.glVertex3f(w0.x1, w0.y1, w0.z1);
			gl.glColor3f(w1z1, w1z1, w1z1);
			gl.glTexCoord2d(1, 1);
			gl.glVertex3f(w1.x1, w1.y1, w1.z1);
			gl.glColor3f(w1z0, w1z0, w1z0);
			gl.glTexCoord2d(1, 0);
			gl.glVertex3f(w1.x0, w1.y0, w1.z0);
			gl.glEnd();
			// gl.glFlush();
		}
		
		public boolean pointOver(float x, float z) {
			float w0z0 = getBrightness((int) Math.abs(w0.z0 - z));
			if(w0z0 < 1.0 / 255.0) return false;
			if(p.contains(Math.round(x * scalePoly), Math.round(z * scalePoly))) return true;
			return false;
		}
		
		public float getY() {
			return (w0.y0 + w0.y1) / 2.0f;
		}

	}
	
	public class Ground implements Drawable {

		@Override
		public void display(GL2 gl, float z) {
			groundTexture.enable(gl);
			groundTexture.bind(gl);
			gl.glBegin(GL2.GL_POLYGON);
			gl.glColor3f(1.0f, 1.0f, 1.0f);
			gl.glTexCoord2d(0, 0);
			gl.glVertex3f(-maxZ, minRoadY - 1.0f, 0);
			gl.glTexCoord2d(0, 1);
			gl.glVertex3f(maxZ, minRoadY - 1.0f, 0);
			gl.glColor3f(0.0f, 0.0f, 0.0f);
			gl.glTexCoord2d(1, 1);
			gl.glVertex3f(maxZ, minRoadY - 1.0f, maxZ);
			gl.glTexCoord2d(1, 0);
			gl.glVertex3f(-maxZ, minRoadY - 1.0f, maxZ);
			gl.glEnd();
		}

		@Override
		public boolean pointOver(float x, float z) {
			// TODO Auto-generated method stub
			return false;
		}

		@Override
		public float getY() {
			// TODO Auto-generated method stub
			return 0;
		}
		
	}

	public Road(GLCanvas parent) {
		this.parent = parent;
		parent.getContext().makeCurrent();
		ArrayList<Float> thetaVals = new ArrayList<Float>();
		ArrayList<Float> phiVals = new ArrayList<Float>();
		ArrayList<Float> thetaZVals = new ArrayList<Float>();
		ArrayList<Float> phiZVals = new ArrayList<Float>();
		initRoadObjects();
		float currentTheta = 0.0f;
		float currentPhi = 0.0f;
		for (float z = 0.0f; z < maxZ; z += (zStep + Math.random()) * 10.f) {
			thetaZVals.add(z);
			thetaVals.add(currentTheta);
			int randInt = randomGenerator.nextInt(8);
			if(randInt >= 2 && randInt <= 5) randInt = 4;
			currentTheta = (float) (randInt * Math.PI / 16.0 - Math.PI / 4.0);
		}
		for (float z = 0.0f; z < maxZ; z += (zStep * 5.0f) + Math.random() * 5.0f) {
			phiZVals.add(z);
			phiVals.add(currentPhi);
			int randInt = randomGenerator.nextInt(8);
			if(randInt >= 2 && randInt <= 5) randInt = 4;
			float deltaPhi = (float) (randInt * Math.PI / 16.0 - Math.PI / 4.0);
			currentPhi += deltaPhi;
			if (currentPhi < -0.125f * (float) Math.PI)
				currentPhi = -0.125f * (float) Math.PI;
			if (currentPhi > 0.125f * (float) Math.PI)
				currentPhi = 0.125f * (float) Math.PI;
		}
		distToTheta = getCubicSpline(thetaZVals, thetaVals);
		distToPhi = getCubicSpline(phiZVals, phiVals);
		generateRoad(0, 0, 0);
		parent.getContext().release();
	}
	
	public void draw(GL2 gl, float x, float y, float z) {
		Ground ground = new Ground();
		ground.display(gl, z);
		for(int index: textureIndexToSurfaces.keySet()) {
			Texture surfaceTexture = surfaceTextures.get(index);
			surfaceTexture.enable(gl);
			surfaceTexture.bind(gl);
			for(Surface surface: textureIndexToSurfaces.get(index)) {
				surface.display(gl, z);
			}
			//surfaceTexture.disable(gl);
		}
		for(int index: textureIndexToInnerWalls.keySet()) {
			Texture wallTexture = innerWallTextures.get(index);
			wallTexture.enable(gl);
			wallTexture.bind(gl);
			for(Wall wall: textureIndexToInnerWalls.get(index)) {
				wall.display(gl, z);
			}
			//wallTexture.disable(gl);
		}
		for(int index: textureIndexToOuterWalls.keySet()) {
			Texture wallTexture = outerWallTextures.get(index);
			wallTexture.enable(gl);
			wallTexture.bind(gl);
			for(Wall wall: textureIndexToOuterWalls.get(index)) {
				wall.display(gl, z);
			}
			//wallTexture.disable(gl);
		}
	}
	
	public ArrayList<Surface> getSurfaces() {
		ArrayList<Surface> returnVal = new ArrayList<Surface>();
		for(int index: textureIndexToSurfaces.keySet()) {
			for(Surface surface: textureIndexToSurfaces.get(index)) {
				returnVal.add(surface);
			}
		}
		return returnVal;
	}
	
	public ArrayList<Wall> getWalls() {
		ArrayList<Wall> returnVal = new ArrayList<Wall>();
		for(int index: textureIndexToInnerWalls.keySet()) {
			for(Wall wall: textureIndexToInnerWalls.get(index)) {
				returnVal.add(wall);
			}
		}
		return returnVal;
	}
	
	public float getY(float z) {
		return zToY.interpolate(z);
	}
	
	public float getLeftX(float z) {
		return zToLeftX.interpolate(z);
	}
	
	public float getRightX(float z) {
		return zToRightX.interpolate(z);
	}
	
	public float getTheta(float z) {
		return zToTheta.interpolate(z);
	}
	
	public float getPhi(float z) {
		return zToPhi.interpolate(z);
	}
		

	public void generateRoad(float x, float y, float z) {
		float roadZ = 0.0f;
		float roadY = 0.0f;
		float roadX = 0.0f;
		float theta = (float) distToTheta.interpolate(0.0f);
		float phi = (float) distToPhi.interpolate(0.0f);
		ArrayList<Float> rightXVals = new ArrayList<Float>();
		ArrayList<Float> leftXVals = new ArrayList<Float>();
		ArrayList<Float> yVals = new ArrayList<Float>();
		ArrayList<Float> zVals = new ArrayList<Float>();
		ArrayList<Float> thetaVals = new ArrayList<Float>();
		ArrayList<Float> phiVals = new ArrayList<Float>();
		zVals.add(roadZ);
		yVals.add(roadY);
		rightXVals.add(roadX + roadWidth / 2.0f);
		leftXVals.add(roadX - roadWidth / 2.0f);
		thetaVals.add(theta);
		phiVals.add(phi);
		float innerZStep = 2.0f;
		for (float innerZ = 0.0f; innerZ < maxZ; innerZ += 2.0f) {
			int surfaceTextureIndex = Math.round(innerZ / innerZStep) % 2;
			int outerWallTextureIndex = Math.round(innerZ / innerZStep) % 2;
			int innerWallTextureIndex = Math.round(innerZ / innerZStep) % 2;
			float frontLeftX = (float) (roadX - roadWidth / 2.0);
			float frontRightX = (float) (roadX + roadWidth / 2.0);
			float frontYBottom = roadY;
			float frontZ = roadZ;
			roadX += Math.sin(theta) * zStep / 5.0f;
			roadY += Math.sin(phi) * zStep / 5.0f;
			if(roadY < minRoadY) minRoadY = roadY;
			roadZ += Math.cos(theta) * zStep / 5.0f;
			float backLeftX = roadX - roadWidth / 2.0f;
			float backRightX = roadX + roadWidth / 2.0f;
			float backYBottom = roadY;
			float backZ = roadZ;
			Wall w0 = new Wall(frontLeftX, frontYBottom, frontZ, backLeftX, backYBottom, backZ, markerHeight, -markerDepth, innerWallTextureIndex);
			Wall w1 = new Wall(frontRightX, frontYBottom, frontZ, backRightX, backYBottom, backZ, markerHeight, markerDepth, innerWallTextureIndex);
			Wall w01 = new Wall(frontLeftX - markerDepth, frontYBottom + markerHeight, frontZ, backLeftX - markerDepth, backYBottom + markerHeight, backZ, markerHeight / 2.0f, 0, outerWallTextureIndex);
			Wall w11 = new Wall(frontRightX + markerDepth, frontYBottom + markerHeight, frontZ, backRightX + markerDepth, backYBottom + markerHeight, backZ, markerHeight / 2.0f, 0, outerWallTextureIndex);
			Surface s = new Surface(w0, w1, surfaceTextureIndex);
			textureIndexToSurfaces.get(surfaceTextureIndex).add(s);
			textureIndexToInnerWalls.get(innerWallTextureIndex).add(w0);
			textureIndexToInnerWalls.get(innerWallTextureIndex).add(w1);
			textureIndexToOuterWalls.get(outerWallTextureIndex).add(w01);
			textureIndexToOuterWalls.get(outerWallTextureIndex).add(w11);
			theta = (float) distToTheta.interpolate(innerZ);
			phi = (float) distToPhi.interpolate(innerZ);
			zVals.add(roadZ);
			yVals.add(backYBottom);
			leftXVals.add(backLeftX);
			rightXVals.add(backRightX);
			thetaVals.add(theta);
			phiVals.add(phi);
		}
		zToLeftX = getCubicSpline(zVals, leftXVals);
		zToRightX = getCubicSpline(zVals, rightXVals);
		zToY = getCubicSpline(zVals, yVals);
		zToTheta = getCubicSpline(zVals, thetaVals);
		zToPhi = getCubicSpline(zVals, phiVals);
		// System.out.println(returnVal.size());
		// return returnVal;
	}
	
	public CubicSpline getCubicSpline(ArrayList<Float> zVals, ArrayList<Float> xVals) {
		float[] zValsArray = new float[zVals.size()];
		float[] xValsArray = new float[xVals.size()];
		int index = 0;
		for (float zVal : zVals) {
			zValsArray[index] = zVal;
			index++;
		}
		index = 0;
		for (float xVal : xVals) {
			xValsArray[index] = xVal;
			index++;
		}
		return new CubicSpline(zValsArray, xValsArray);
	}

	public void initRoadObjects() {
		//if(!initObjects) return;
		initDeltaZToBrightness();
		createSurfaceTextures();
		createInnerWallTextures();
		createOuterWallTextures();
		createGroundTexture();
		//initObjects = false;
	}
	
	public void createSurfaceTextures() {
    	BufferedImage img1 = null;
    	BufferedImage img2 = null;
    	try {
    	    img1 = ImageIO.read(new File("images/Surface1.jpg"));
    	    img2 = ImageIO.read(new File("images/Surface2.jpg"));
    	} catch (IOException e) {
    		System.out.println("Road.createSurfaceTextures: image not found");
    	}
    	textureIndexToSurfaces = new TreeMap<Integer, ArrayList<Surface>>();
    	surfaceTextures = new ArrayList<Texture>();
    	createSurfaceTextures(img1, 0);
    	createSurfaceTextures(img2, 1);
	}
	
    public void createSurfaceTextures(BufferedImage img, int index) {
    	int width = img.getWidth();
    	int height = img.getHeight();
    	int[] mem = new int[width * height];
    	BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    	Graphics2D g2 = bi.createGraphics();
    	for(int x = 0; x < width; x += 1) {
    		for(int y = 0; y < height; y += 1) {
    			int randVal = randomGenerator.nextInt(128) + 128;
    			Color color = new Color(randVal, randVal, randVal, 128);
    			int value = color.hashCode();
    			mem[x + y * width] = value;
    		}
    	}
        BufferedImage img2= new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        img2.setRGB(0, 0, width, height, mem, 0, width);
        int rule = AlphaComposite.SRC_OVER;
    	Composite comp = AlphaComposite.getInstance(rule , 0.5f );
    	g2.setComposite(comp );
    	g2.drawImage(img, 0, 0, null);
       	g2.drawImage(img2, 0, 0, null);
    	surfaceTextures.add(AWTTextureIO.newTexture(parent.getGLProfile(), bi, true));
    	textureIndexToSurfaces.put(index, new ArrayList<Surface>());
    }
    
	public void createInnerWallTextures() {
    	BufferedImage img1 = null;
    	BufferedImage img2 = null;
    	try {
    	    img1 = ImageIO.read(new File("images/InnerWall1.jpg"));
    	    img2 = ImageIO.read(new File("images/InnerWall2.jpg"));
    	} catch (IOException e) {
    		System.out.println("Road.createSurfaceTextures: image not found");
    	}
    	textureIndexToInnerWalls = new TreeMap<Integer, ArrayList<Wall>>();
    	innerWallTextures = new ArrayList<Texture>();
    	createInnerWallTextures(img1, 0);
    	createInnerWallTextures(img2, 1);
	}
    
    public void createInnerWallTextures(BufferedImage img, int index) {
    	int width = img.getWidth();
    	int height = img.getHeight();
    	int[] mem = new int[width * height];
    	BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    	Graphics2D g2 = bi.createGraphics();
    	for(int x = 0; x < width; x += 1) {
    		for(int y = 0; y < height; y += 1) {
    			int randVal = randomGenerator.nextInt(128) + 128;
    			Color color = new Color(randVal, randVal, randVal, 128);
    			int value = color.hashCode();
    			mem[x + y * width] = value;
    		}
    	}
        BufferedImage img2= new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        img2.setRGB(0, 0, width, height, mem, 0, width);
        int rule = AlphaComposite.SRC_OVER;
    	Composite comp = AlphaComposite.getInstance(rule , 0.5f );
    	g2.setComposite(comp );
    	g2.drawImage(img, 0, 0, null);
       	g2.drawImage(img2, 0, 0, null);
    	innerWallTextures.add(AWTTextureIO.newTexture(parent.getGLProfile(), bi, true));
   		textureIndexToInnerWalls.put(index, new ArrayList<Wall>());
    }
    
    public void createOuterWallTextures() {
    	BufferedImage img1 = null;
    	BufferedImage img2 = null;
    	try {
    	    img1 = ImageIO.read(new File("images/OuterWall1.jpg"));
    	    img2 = ImageIO.read(new File("images/OuterWall2.jpg"));
    	} catch (IOException e) {
    		System.out.println("Road.createSurfaceTextures: image not found");
    	}
    	textureIndexToOuterWalls = new TreeMap<Integer, ArrayList<Wall>>();
    	outerWallTextures = new ArrayList<Texture>();
    	createOuterWallTextures(img1, 0);
    	createOuterWallTextures(img2, 1);
    }
    
    public void createOuterWallTextures(BufferedImage img, int index) {
    	int width = img.getWidth();
    	int height = img.getHeight();
    	int[] mem = new int[width * height];
    	BufferedImage bi = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    	Graphics2D g2 = bi.createGraphics();
    	for(int x = 0; x < width; x += 1) {
    		for(int y = 0; y < height; y += 1) {
      			int randVal = randomGenerator.nextInt(128) + 128;
    			Color color = new Color(randVal, randVal, randVal, 128);
    			int value = color.hashCode();
    			mem[x + y * width] = value;
    		}
    	}
        BufferedImage img2= new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        img2.setRGB(0, 0, width, height, mem, 0, width);
        int rule = AlphaComposite.SRC_OVER;
    	Composite comp = AlphaComposite.getInstance(rule , 0.5f );
    	g2.setComposite(comp );
    	g2.drawImage(img, 0, 0, null);
       	g2.drawImage(img2, 0, 0, null);
    	outerWallTextures.add(AWTTextureIO.newTexture(parent.getGLProfile(), bi, true));
    	textureIndexToOuterWalls.put(index, new ArrayList<Wall>());
    }
    
    public void createGroundTexture() {
    	BufferedImage bi = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
    	Graphics2D g2 = bi.createGraphics();
    	float red = 1.0f;
    	float green = 1.0f;
    	float blue = 0.0f;
    	for(int x = 0; x < 100; x++) {
    		for(int y = 0; y < 100; y++) {
    			float brightness = (float) Math.random();
    			g2.setColor(new Color(brightness * red, brightness * green, brightness * blue));
    			g2.drawRect(x, y, x, y);
    		}
    	}
    	groundTexture = (AWTTextureIO.newTexture(GLProfile.getDefault(), bi, true));
    }
    
    private void initDeltaZToBrightness() {
    	deltaZToBrightness = new TreeMap<Integer, Float>();
    	for(int deltaZ = 0; deltaZ <= maxZ; deltaZ++) {
    		deltaZToBrightness.put(deltaZ, (float) Math.pow(brightnessScalar / deltaZ, brightnessMantissa));
    	}
    }
    
    private float getBrightness(int deltaZ) {
    	if(constantBrightness) return 1.0f;
    	if(deltaZ > maxViewableZ || deltaZ < 0) return 0.0f;
    	float brightness = deltaZToBrightness.get(deltaZ);
    	if(brightness < 8.0f / 255f) return 0.0f;
    	return brightness;
    }

}
